{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "82169ccb",
   "metadata": {},
   "source": [
    "## Conducting Walk-Forward Optimization with VectorBT"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "d789ce16",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import scipy.stats as stats\n",
    "import vectorbt as vbt\n",
    "from IPython.display import Markdown, display"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1f282989",
   "metadata": {},
   "source": [
    "Define the start and end dates for data download"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "8780abd4",
   "metadata": {},
   "outputs": [],
   "source": [
    "start = \"2016-01-01 UTC\"\n",
    "end = \"2020-01-01 UTC\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26027af9",
   "metadata": {},
   "source": [
    "Download historical closing prices for the symbol \"AAPL\" from Yahoo Finance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "04c21a95",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices = vbt.YFData.download(\"AAPL\", start=start, end=end).get(\"Close\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca826e83",
   "metadata": {},
   "source": [
    "Split the prices data into in-sample and out-of-sample sets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "51a50070",
   "metadata": {},
   "outputs": [],
   "source": [
    "(in_price, in_indexes), (out_price, out_indexes) = prices.vbt.rolling_split(\n",
    "    n=30,\n",
    "    window_len=365 * 2,\n",
    "    set_lens=(180,),\n",
    "    left_to_right=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f483d1f",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "Function to simulate all parameter combinations and calculate Sharpe ratios"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "63255845",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "def simulate_all_params(price, windows, **kwargs):\n",
    "    fast_ma, slow_ma = vbt.MA.run_combs(\n",
    "        price, windows, r=2, short_names=[\"fast\", \"slow\"]\n",
    "    )\n",
    "    entries = fast_ma.ma_crossed_above(slow_ma)\n",
    "    exits = fast_ma.ma_crossed_below(slow_ma)\n",
    "    pf = vbt.Portfolio.from_signals(price, entries, exits, **kwargs)\n",
    "    return pf.sharpe_ratio()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b28d6a4d",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "Function to get the best index based on performance"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "3356d7c3",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "def get_best_index(performance):\n",
    "    return performance[performance.groupby(\"split_idx\").idxmax()].index"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e2d951e",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "Function to get the best parameters from the best index"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "cc06291e",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "def get_best_params(best_index, level_name):\n",
    "    return best_index.get_level_values(level_name).to_numpy()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18a6b7ce",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "Function to simulate the best parameters and calculate Sharpe ratios"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "89a92a16",
   "metadata": {},
   "outputs": [],
   "source": [
    "def simulate_best_params(price, best_fast_windows, best_slow_windows, **kwargs):\n",
    "    fast_ma = vbt.MA.run(price, window=best_fast_windows, per_column=True)\n",
    "    slow_ma = vbt.MA.run(price, window=best_slow_windows, per_column=True)\n",
    "\n",
    "    entries = fast_ma.ma_crossed_above(slow_ma)\n",
    "    exits = fast_ma.ma_crossed_below(slow_ma)\n",
    "\n",
    "    pf = vbt.Portfolio.from_signals(price, entries, exits, **kwargs)\n",
    "    return pf.sharpe_ratio()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0aba57f",
   "metadata": {},
   "source": [
    "Define the range of windows for moving averages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "32523527",
   "metadata": {},
   "outputs": [],
   "source": [
    "windows = np.arange(10, 40)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "219410d4",
   "metadata": {},
   "source": [
    "Simulate all parameter combinations for in-sample data and calculate Sharpe ratios"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "51fac2e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "in_sharpe = simulate_all_params(in_price, windows, direction=\"both\", freq=\"d\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65fde50e",
   "metadata": {},
   "source": [
    "Get the best index and parameters from the in-sample Sharpe ratios"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "389d0677",
   "metadata": {},
   "outputs": [],
   "source": [
    "in_best_index = get_best_index(in_sharpe)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "56153ada",
   "metadata": {},
   "outputs": [],
   "source": [
    "in_best_fast_windows = get_best_params(in_best_index, \"fast_window\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "4def22b0",
   "metadata": {},
   "outputs": [],
   "source": [
    "in_best_slow_windows = get_best_params(in_best_index, \"slow_window\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "e3896614",
   "metadata": {},
   "outputs": [],
   "source": [
    "in_best_window_pairs = np.array(list(zip(in_best_fast_windows, in_best_slow_windows)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "68372e8d",
   "metadata": {},
   "source": [
    "Simulate the best parameters for out-of-sample data and calculate Sharpe ratios"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "b303fbe2",
   "metadata": {},
   "outputs": [],
   "source": [
    "out_test_sharpe = simulate_best_params(\n",
    "    out_price, in_best_fast_windows, in_best_slow_windows, direction=\"both\", freq=\"d\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "d7a41e3e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ma_window  ma_window  split_idx\n",
       "10         11         0            0.104948\n",
       "12         13         1            0.318327\n",
       "                      2            0.971232\n",
       "10         11         3            1.386777\n",
       "12         13         4            1.303281\n",
       "10         11         5            2.133288\n",
       "                      6            2.043516\n",
       "18         23         7            1.756907\n",
       "                      8            2.219373\n",
       "                      9            2.283883\n",
       "                      10           2.543999\n",
       "                      11           2.724649\n",
       "                      12           2.389901\n",
       "                      13           2.838679\n",
       "                      14           2.393304\n",
       "                      15           1.116309\n",
       "23         26         16           0.670555\n",
       "                      17           0.594145\n",
       "                      18           0.816461\n",
       "24         25         19           0.276339\n",
       "                      20          -0.052507\n",
       "                      21          -0.363493\n",
       "                      22          -0.895639\n",
       "                      23          -0.665974\n",
       "                      24          -0.147559\n",
       "                      25           0.369170\n",
       "                      26           0.710684\n",
       "                      27           0.463619\n",
       "                      28           1.064843\n",
       "                      29           1.437157\n",
       "Name: sharpe_ratio, dtype: float64"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(out_test_sharpe)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67d30b8e",
   "metadata": {},
   "source": [
    "Perform a t-test to compare the in-sample and out-of-sample Sharpe ratios"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "51b385d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "in_sample_best = in_sharpe[in_best_index].values\n",
    "out_sample_test = out_test_sharpe.values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "3e989b76",
   "metadata": {},
   "outputs": [],
   "source": [
    "t, p = stats.ttest_ind(a=out_sample_test, b=in_sample_best, alternative=\"greater\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "616c9a88",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "-1.0849398748573142"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "0.8587812940925162"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(t, p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "6dfd8345",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ma_window  ma_window  split_idx\n",
       "10         11         0            0.104948\n",
       "12         13         1            0.318327\n",
       "                      2            0.971232\n",
       "10         11         3            1.386777\n",
       "12         13         4            1.303281\n",
       "10         11         5            2.133288\n",
       "                      6            2.043516\n",
       "18         23         7            1.756907\n",
       "                      8            2.219373\n",
       "                      9            2.283883\n",
       "                      10           2.543999\n",
       "                      11           2.724649\n",
       "                      12           2.389901\n",
       "                      13           2.838679\n",
       "                      14           2.393304\n",
       "                      15           1.116309\n",
       "23         26         16           0.670555\n",
       "                      17           0.594145\n",
       "                      18           0.816461\n",
       "24         25         19           0.276339\n",
       "                      20          -0.052507\n",
       "                      21          -0.363493\n",
       "                      22          -0.895639\n",
       "                      23          -0.665974\n",
       "                      24          -0.147559\n",
       "                      25           0.369170\n",
       "                      26           0.710684\n",
       "                      27           0.463619\n",
       "                      28           1.064843\n",
       "                      29           1.437157\n",
       "Name: sharpe_ratio, dtype: float64"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(out_test_sharpe)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c069fd5e",
   "metadata": {},
   "source": [
    "Defines the alternative hypothesis. The following options are available (default is ‘two-sided’):\n",
    "*  ‘two-sided’: the means of the distributions underlying the samples are unequal.\n",
    "*  ‘less’: the mean of the distribution underlying the first sample is less than the mean of the distribution underlying the second sample.\n",
    "*  ‘greater’: the mean of the distribution underlying the first sample is greater than the mean of the distribution underlying the second sample."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "efa0ecac",
   "metadata": {},
   "source": [
    "**Jason Strimpel** is the founder of <a href='https://pyquantnews.com/'>PyQuant News</a> and co-founder of <a href='https://www.tradeblotter.io/'>Trade Blotter</a>. His career in algorithmic trading spans 20+ years. He previously traded for a Chicago-based hedge fund, was a risk manager at JPMorgan, and managed production risk technology for an energy derivatives trading firm in London. In Singapore, he served as APAC CIO for an agricultural trading firm and built the data science team for a global metals trading firm. Jason holds degrees in Finance and Economics and a Master's in Quantitative Finance from the Illinois Institute of Technology. His career spans America, Europe, and Asia. He shares his expertise through the <a href='https://pyquantnews.com/subscribe-to-the-pyquant-newsletter/'>PyQuant Newsletter</a>, social media, and has taught over 1,000+ algorithmic trading with Python in his popular course **<a href='https://gettingstartedwithpythonforquantfinance.com/'>Getting Started With Python for Quant Finance</a>**. All code is for educational purposes only. Nothing provided here is financial advise. Use at your own risk."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24ac93d2-a004-40e5-8937-bef8fd25c6f1",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "jupytext": {
   "cell_metadata_filter": "-all",
   "main_language": "python",
   "notebook_metadata_filter": "-all"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
